﻿/* ****************************************************************************
 *
 * Copyright (c) Microsoft Corporation. 
 *
 * This source code is subject to terms and conditions of the Microsoft Public License. A
 * copy of the license can be found at http://go.microsoft.com/fwlink/?LinkID=131993. If
 * you cannot locate the Microsoft Public License, please send an email to
 * mixon@microsoft.com. By using this source code in any fashion, you are agreeing to
 * be bound by the terms of the Microsoft Public License.
 *
 * You must not remove this notice, or any other, from this software.
 *
 *
 * ***************************************************************************/

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace Microsoft.Controls
{
    public class VirtualizedListBox : VirtualizedItemsControl
    {
        private bool processingSelectionPropertyChange;
        private bool readOnlyDependencyPropertyChangesAllowed;
        private int focusedIndex = -1;
        private bool isFocusedVisible = true;

        public event SelectionChangedEventHandler SelectionChanged;

        public object SelectedItem
        {
            get { return (object)GetValue(SelectedItemProperty); }
            set { SetValue(SelectedItemProperty, value); }
        }

        public static readonly DependencyProperty SelectedItemProperty =
            DependencyProperty.Register("SelectedItem", typeof(object), typeof(VirtualizedListBox), new PropertyMetadata(OnSelectedItemChanged));

        public int SelectedIndex
        {
            get { return (int)GetValue(SelectedIndexProperty); }
            set { SetValue(SelectedIndexProperty, value); }
        }

        public static readonly DependencyProperty SelectedIndexProperty =
            DependencyProperty.Register("SelectedIndex", typeof(int), typeof(VirtualizedListBox), new PropertyMetadata(OnSelectedIndexChanged));

        public static readonly DependencyProperty IsSelectionActiveProperty = DependencyProperty.RegisterAttached("IsSelectionActive", typeof(bool),
            typeof(VirtualizedListBox), new PropertyMetadata(OnIsSelectionActiveChanged));


        public VirtualizedListBox()
        {
            this.IsTabStop = false;
            this.KeyDown += delegate(object sender, KeyEventArgs e)
            {
                this.OnKeyDown(e);
            };
            this.GotFocus += delegate(object sender, RoutedEventArgs e)
            {
                this.OnGotFocus(e);
            };
            this.LostFocus += delegate(object sender, RoutedEventArgs e)
            {
                this.OnLostFocus(e);
            };
        }

        private static void OnSelectedIndexChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var listBox = (VirtualizedListBox)sender;
            listBox.OnSelectedIndexChanged((int)e.OldValue, (int)e.NewValue);
        }

        private void OnSelectedIndexChanged(int oldValue, int newValue)
        {
            object oldValueItem = ((oldValue != -1) && (oldValue < this.Items.Count)) ? this.Items[oldValue] : null;
            object newValueItem = (newValue != -1) ? this.Items[newValue] : null;
            ProcessSelectionPropertyChange(SelectedIndexProperty, oldValueItem, newValueItem);
        }

        private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var listBox = (VirtualizedListBox)sender;
            listBox.OnSelectedItemChanged(e.OldValue, e.NewValue);
        }

        private void OnSelectedItemChanged(object oldValue, object newValue)
        {
            this.ProcessSelectionPropertyChange(SelectedItemProperty, oldValue, newValue);
        }

        private void ProcessSelectionPropertyChange(DependencyProperty changedProperty, object oldValue, object newValue)
        {
            // Avoid recursion.
            if (!this.processingSelectionPropertyChange)
            {
                try
                {
                    this.processingSelectionPropertyChange = true;

                    // Trace the removed/added items for SelectionChanged.
                    List<object> removedItems = new List<object>();
                    List<object> addedItems = new List<object>();

                    // If old value is present, update the associated ListBoxItem.
                    if (null != oldValue)
                    {
                        var oldListBoxItem = this.ItemsPanel.GetContainerForItem(oldValue) as VirtualizedListBoxItem;
                        if (null != oldListBoxItem)
                        {
                            oldListBoxItem.IsSelected = false;
                        }
                        removedItems.Add(oldValue);
                    }

                    // If new value is present, update the associated ListBoxItem.
                    object newSelectedItem = null;
                    int newSelectedIndex = -1;
                    if (null != newValue)
                    {
                        var newListBoxItem = this.ItemsPanel.GetContainerForItem(newValue) as VirtualizedListBoxItem;
                        if (null != newListBoxItem)
                        {
                            newListBoxItem.IsSelected = true;
                        }
                        newSelectedItem = newValue;
                        newSelectedIndex = Items.IndexOf(newSelectedItem);
                        addedItems.Add(newValue);
                    }

                    // Update the *other* associated ListBox properties.
                    if (SelectedIndexProperty != changedProperty)
                    {
                        this.SelectedIndex = newSelectedIndex;
                    }
                    if (SelectedItemProperty != changedProperty)
                    {
                        this.SelectedItem = newSelectedItem;
                    }

                    // Notify of SelectionChanged.
                    this.OnSelectionChanged(new SelectionChangedEventArgs(removedItems, addedItems));
                }
                finally
                {
                    this.processingSelectionPropertyChange = false;
                }
            }
        }

        private static void OnIsSelectionActiveChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var virtualizedListBox = d as VirtualizedListBox;
            // Since this is an attached property, it could be attached to something other than a ListBox.
            // If that's the case, we shouldn't crash. We should ignore the fact that this property was set.
            if (virtualizedListBox != null)
            {
                virtualizedListBox.OnIsSelectionActiveChanged((bool)e.OldValue, (bool)e.NewValue);
            }
        }

        internal virtual void OnIsSelectionActiveChanged(bool oldValue, bool newValue)
        {
            if (readOnlyDependencyPropertyChangesAllowed)
            {
                if (null != SelectedItem)
                {
                    var selectedListBoxItem = this.ItemsPanel.GetContainerForItem(SelectedItem) as VirtualizedListBoxItem;
                    if (null != selectedListBoxItem)
                    {
                        selectedListBoxItem.ChangeVisualState();
                    }
                }
            }
            else
            {
                throw new InvalidOperationException("IsSelectionActive is a read-only attached property and therefore it can not be changed.");
            }
        }

        private void OnSelectionChanged(SelectionChangedEventArgs e)
        {
            SelectionChangedEventHandler eventHandler = this.SelectionChanged;
            if (eventHandler != null)
            {
                eventHandler(this, e);
            }
        }

        public override UIElement GetContainerForItemOverride()
        {
            return new VirtualizedListBoxItem();
        }

        public override void PrepareContainerForItemOverride(UIElement container, object dataItem)
        {
            var containerListBoxItem = container as VirtualizedListBoxItem;
            if (containerListBoxItem != null)
            {
                // First time this container is prepared.
                if (containerListBoxItem.ContentTemplate != this.ItemTemplate)
                {
                    containerListBoxItem.ContentTemplate = this.ItemTemplate;
                    containerListBoxItem.ParentListBox = this;
                }
                // Changing the data on this container.
                if (containerListBoxItem.Content != dataItem)
                {
                    containerListBoxItem.Content = dataItem;
                    containerListBoxItem.DataContext = dataItem;

                    containerListBoxItem.IsSelected = Object.Equals(dataItem, this.SelectedItem);

                    int currentIndex = this.Items.IndexOf(dataItem);
                    if (currentIndex == this.focusedIndex)
                    {
                        containerListBoxItem.Focus();
                    }
                }
            }
        }

        // compatible with WPF and Silverlight.
        public override void ClearContainerForItemOverride(UIElement container, object dataItem)
        {
            var containerListBoxItem = container as VirtualizedListBoxItem;
            if (containerListBoxItem != null)
            {
                containerListBoxItem.ContentTemplate = null;
                containerListBoxItem.ParentListBox = null;
                containerListBoxItem.Content = null;
                containerListBoxItem.DataContext = null;
                containerListBoxItem.IsSelected = false;
            }
        }

        internal void NotifyListItemClicked(VirtualizedListBoxItem listBoxItem)
        {
            if (listBoxItem.IsSelected)
            {
                if (ModifierKeys.Control == (Keyboard.Modifiers & ModifierKeys.Control))
                {
                    this.SelectedItem = null;
                }
            }
            else
            {
                this.SelectContainerAndFocus(listBoxItem);
                this.ScrollIntoView(listBoxItem.Content); // The item could be partially visible.
            }
        }

        // Select data item and give its container focus.
        private void SelectDataItemAndFocus(object dataItem, int dataItemIndex)
        {
            this.SelectedItem = dataItem;
            if (null != dataItem)
            {
                var container = this.ItemsPanel.GetContainerForItem(dataItem) as Control;
                if (container != null)
                {
                    // This sets focus on the LBI, which in turn will call NotifyListItemGotFocus in the LB.
                    container.Focus();
                }
                // If the data item is not visible, set focusedIndex so that in the next layout pass the item will be given focus.
                else
                {
                    this.focusedIndex = dataItemIndex;
                    this.isFocusedVisible = this.IsIndexVisible(dataItemIndex);
                }
            }
        }

        private void SelectContainerAndFocus(VirtualizedListBoxItem lbi)
        {
            this.SelectedItem = lbi.Content;
            lbi.Focus();
        }

        public void ScrollIntoView(object item)
        {
            this.ScrollIntoView(item, null);
        }

        private void ScrollIntoView(object item, bool? last)
        {
            var desiredIndex = this.Items.IndexOf(item);
            var lastFullyVisibleIndex = this.FirstVisibleItemIndex + this.FullyVisibleItemCount - 1;

            if (desiredIndex != -1)
            {
                if (last == true)
                {
                    this.FirstVisibleItemIndex = desiredIndex - this.FullyVisibleItemCount + 1;
                }
                else if (last == false)
                {
                    this.FirstVisibleItemIndex = desiredIndex;
                }
                else if (last == null && !this.IsIndexFullyVisible(desiredIndex))
                {
                    if (desiredIndex < this.FirstVisibleItemIndex)
                    {
                        this.FirstVisibleItemIndex = desiredIndex;
                    }
                    else if (desiredIndex > lastFullyVisibleIndex)
                    {
                        this.FirstVisibleItemIndex = desiredIndex - this.FullyVisibleItemCount + 1;
                    }
                }
            }
        }

        public static bool GetIsSelectionActive(DependencyObject element)
        {
            if (null == element)
            {
                throw new ArgumentNullException("element");
            }
            return (bool)element.GetValue(IsSelectionActiveProperty);
        }

        private static void SetIsSelectionActive(DependencyObject element, bool value)
        {
            var listBox = element as VirtualizedListBox;
            try
            {
                listBox.readOnlyDependencyPropertyChangesAllowed = true;
                listBox.SetValue(IsSelectionActiveProperty, value);
            }
            finally
            {
                listBox.readOnlyDependencyPropertyChangesAllowed = false;
            }
        }

        /// <summary>
        /// Called when the control got focus.
        /// </summary>
        /// <param name="e">The event data.</param>
        internal virtual void OnGotFocus(RoutedEventArgs e)
        {
            SetIsSelectionActive(this, true);
        }

        /// <summary>
        /// Called when the control lost focus.
        /// </summary>
        /// <param name="e">The event data.</param>
        internal virtual void OnLostFocus(RoutedEventArgs e)
        {
            SetIsSelectionActive(this, false);
        }

        internal void NotifyListItemGotFocus(VirtualizedListBoxItem listBoxItemNewFocus)
        {
            this.focusedIndex = this.Items.IndexOf(listBoxItemNewFocus.Content);
            this.isFocusedVisible = this.IsIndexVisible(this.focusedIndex);
        }

        internal void NotifyListItemLostFocus(VirtualizedListBoxItem listBoxItemOldFocus)
        {
            this.focusedIndex = -1;
            this.isFocusedVisible = false;
            if (listBoxItemOldFocus == this.ItemsPanel.FocusedContainer)
            {
                this.ClearContainerForItemOverride(this.ItemsPanel.FocusedContainer, ((VirtualizedListBoxItem)this.ItemsPanel.FocusedContainer).Content);
            }
        }

        internal virtual void OnKeyDown(KeyEventArgs e)
        {
            if (!e.Handled)
            {
                bool handled = false;
                int newFocusedIndex = -1;
                bool? pageDown = null;
                switch (e.Key)
                {
                    // Ctrl + Enter or Ctrl + Space = toggle selection
                    // Enter or Space = select
                    case Key.Space:
                    case Key.Enter:
                        if ((Key.Enter != e.Key) /*|| KeyboardNavigation.GetAcceptsReturn(this)*/)
                        {
                            // Ctrl+Alt, Ctrl, or neither (not Alt by itself)
                            if (ModifierKeys.Alt != (Keyboard.Modifiers & (ModifierKeys.Control | ModifierKeys.Alt)))
                            {
                                // KeyEventArgs.OriginalSource (used by WPF) isn't available in Silverlight; use FocusManager.GetFocusedElement instead
                                var listBoxItem = FocusManager.GetFocusedElement() as VirtualizedListBoxItem;
                                if (null != listBoxItem)
                                {
                                    // Is Ctrl pressed
                                    if ((ModifierKeys.Control == (Keyboard.Modifiers & ModifierKeys.Control)) && listBoxItem.IsSelected)
                                    {
                                        this.SelectedItem = null;
                                    }
                                    else
                                    {
                                        this.SelectContainerAndFocus(listBoxItem);
                                    }
                                    handled = true;
                                }
                            }
                        }
                        break;
                    case Key.Home:
                        newFocusedIndex = 0;
                        break;
                    case Key.End:
                        newFocusedIndex = this.Items.Count - 1;
                        break;
                    case Key.PageUp:
                        newFocusedIndex = this.NavigateByPage(false);
                        pageDown = false;
                        break;
                    case Key.PageDown:
                        newFocusedIndex = this.NavigateByPage(true);
                        pageDown = true;
                        break;
                    case Key.Left:
                        this.ScrollHorizontally(false);
                        break;
                    case Key.Up:
                        newFocusedIndex = this.focusedIndex - 1;
                        break;
                    case Key.Right:
                        this.ScrollHorizontally(true);
                        break;
                    case Key.Down:
                        newFocusedIndex = this.focusedIndex + 1;
                        break;
                    default:
                        break;
                }
                if ((-1 != newFocusedIndex) &&
                    (-1 != this.focusedIndex) &&
                    (newFocusedIndex != this.focusedIndex) &&
                    (0 <= newFocusedIndex) &&
                    (newFocusedIndex < this.Items.Count))
                {
                    var dataItem = this.Items[newFocusedIndex];
                    this.ScrollIntoView(dataItem, pageDown);
                    this.SelectDataItemAndFocus(dataItem, newFocusedIndex);
                    handled = true;
                }
                if (handled)
                {
                    e.Handled = true;
                }
            }
        }

        private int NavigateByPage(bool forward)
        {
            var newFocusedIndex = -1;
            var isFocusedFullyVisible = this.IsIndexFullyVisible(this.focusedIndex);
            var lastFullyVisibleIndex = this.FirstVisibleItemIndex + this.FullyVisibleItemCount - 1;

            if (forward)
            {
                if (isFocusedFullyVisible && (this.focusedIndex != lastFullyVisibleIndex))
                {
                    newFocusedIndex = lastFullyVisibleIndex;
                }
                else if (this.focusedIndex == lastFullyVisibleIndex)
                {
                    newFocusedIndex = Math.Min(this.focusedIndex + this.FullyVisibleItemCount, this.Items.Count - 1);
                }
                else // isFocusedFullyVisible == false
                {
                    newFocusedIndex = Math.Min(this.focusedIndex + this.FullyVisibleItemCount - 1, this.Items.Count - 1);
                }
            }
            else
            {
                if (isFocusedFullyVisible && (this.focusedIndex != this.FirstVisibleItemIndex))
                {
                    newFocusedIndex = this.FirstVisibleItemIndex;
                }
                else if (this.focusedIndex == this.FirstVisibleItemIndex)
                {
                    newFocusedIndex = Math.Max(this.focusedIndex - this.FullyVisibleItemCount, 0);
                }
                else // isFocusedFullyVisible == false
                {
                    newFocusedIndex = Math.Max(this.focusedIndex - this.FullyVisibleItemCount + 1, 0);
                }
            }
            return newFocusedIndex;
        }

        // TODO: This won't work if the visible item count is exactly the same as the fully visible item count.
        // Need an extra property in the contract: VisibleItemCount.
        // What happens in this case? I think it's ok to keep an extra item around for simplicity.
        private bool IsIndexVisible(int index)
        {
            return (index >= this.FirstVisibleItemIndex && index <= (this.FirstVisibleItemIndex + this.FullyVisibleItemCount));
        }

        private bool IsIndexFullyVisible(int index)
        {
            return (index >= this.FirstVisibleItemIndex && index <= (this.FirstVisibleItemIndex + this.FullyVisibleItemCount - 1));
        }

        // This keeps the focused item alive.
        internal override void OnScrolled(ScrolledEventArgs e)
        {
            base.OnScrolled(e);
            if (this.focusedIndex != -1)
            {
                // Focused item just scrolled out of view
                // At this point, isFocusedVisible hasn't been updated so it contains the same value as before scrolling.
                // focusedIndex doesn't change with scrolling.
                if (this.isFocusedVisible && !this.IsIndexVisible(this.focusedIndex))
                {
                    this.PrepareContainerForItemOverride(this.ItemsPanel.FocusedContainer, this.Items[this.focusedIndex]);
                }
                // Focused item just scrolled into view
                else if (!this.isFocusedVisible && this.IsIndexVisible(this.focusedIndex))
                {
                    this.ClearContainerForItemOverride(this.ItemsPanel.FocusedContainer, ((VirtualizedListBoxItem)this.ItemsPanel.FocusedContainer).Content);
                }
            }
        }
    }
}
